package com.test.lucene;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.document.Document;

import com.test.lucene.helper.Book;
import com.test.lucene.helper.Books;
import com.test.lucene.helper.Constants;
import com.test.lucene.helper.SharedParameters;
import com.test.lucene.helper.WriteParameters;
import com.test.lucene.index.DocumentCollection;

/**
 * Write documents to a Lucene repository.
 * 
 * @author Patrick Kimber
 * 
 */
public final class Write extends Manager {

    /**
     * Write documents to a Lucene repository.
     * 
     * @param args
     *            Command line parameters.
     * @throws LuceneException
     *             If a Lucene operation fails.
     * @throws IOException
     *             If an IO operation fails.
     * @throws InterruptedException
     *             If the thread sleep statement is interrupted.
     */
    public static void main(final String[] args) throws LuceneException,
            IOException, InterruptedException {
        Write write = new Write(args);
        if (write.isValid()) {
            write.run();
        }
    }

    /** The application parameters. */
    private WriteParameters parameters;

    /**
     * Constructor.
     * 
     * @param args
     *            Command line parameters.
     */
    public Write(final String[] args) {
        super(args);
    }

    /**
     * Write documents to a Lucene repository.
     * 
     * @throws LuceneException
     *             If a Lucene operation fails.
     * @throws IOException
     *             If an IO operation fails.
     * @throws InterruptedException
     *             If the thread sleep statement is interrupted.
     */
    public void run() throws LuceneException, IOException, InterruptedException {
        DocumentCollection collection = null;
        try {
            collection = createCollection();
            if (isCreate()) {
                create(collection);
            }
            add(collection);
        } finally {
            if (collection != null) {
                collection.close();
            }
        }
        System.out.println("Complete");
    }

    /**
     * Add test documents to this collection.
     * 
     * @param collection
     *            The Lucene document collection.
     * @throws LuceneException
     *             If a Lucene operation fails.
     * @throws InterruptedException
     *             If the thread sleep statement is interrupted.
     */
    private void add(final DocumentCollection collection)
            throws LuceneException, InterruptedException {
        setFinishTime();
        int count = 1;
        long defaultSleep = getDefaultSleep();
        displayStatus(defaultSleep);
        do {
            for (int i = 0; i < Books.getCount(); ++i) {
                Book book = Books.getBook(i);
                System.out.print(book.getRank() + ", ");
                deleteDocuments(collection, findExisting(collection, book,
                        count));
                collection.update(getDocument(book, count));
                Thread.sleep(defaultSleep);
            }
            ++count;
            System.out.println();
        } while (!isFinished());
        System.out.println();
    }

    /**
     * Check the index. Has this book already been added?
     * 
     * @param collection
     *            The Lucene document collection.
     * @param book
     *            Search for this book in the index.
     * @param count
     *            Use this counter to create a unique key.
     * @return A collection of document numbers.
     * @throws LuceneException
     *             If the search fails.
     */
    private Collection findExisting(final DocumentCollection collection,
            final Book book, final int count) throws LuceneException {
        final String criteria = Constants.KEY + ":" + getKey(book, count);
        Collection result = new ArrayList();
        Collection searchResults = collection.search(criteria);
        for (Iterator i = searchResults.iterator(); i.hasNext();) {
            Map m = (Map) i.next();
            result.add(getDocumentNumber(m));
        }
        return result;
    }

    /**
     * Extract the document number from the map containing the field values.
     * 
     * @param m
     *            A map containing the Lucene field names and values.
     * @return An integer object representing the document number.
     * @throws LuceneException
     *             If we cannot extract the document number from the field
     *             values.
     */
    private Integer getDocumentNumber(final Map m) throws LuceneException {
        String value = (String) m.get(Constants.DOCUMENT_NUMBER);
        if (value == null) {
            throw new LuceneException(
                    "Cannot get document number from field values."
                            + m.toString());
        }
        return new Integer(value);
    }

    /**
     * Delete these documents from the Lucene index.
     * 
     * @param collection
     *            The Lucene document collection.
     * @param documentsToDelete
     *            A collection of document numbers which need to be deleted.
     * @throws LuceneException
     *             If the document cannot be deleted.
     */
    private void deleteDocuments(final DocumentCollection collection,
            final Collection documentsToDelete) throws LuceneException {
        for (Iterator i = documentsToDelete.iterator(); i.hasNext();) {
            Integer number = (Integer) i.next();
            collection.delete(number.intValue());
        }

    }

    /**
     * Display status.
     * 
     * @param defaultSleep
     *            The number of milliseconds to sleep between each request.
     */
    private void displayStatus(final long defaultSleep) {
        System.out.println("Adding " + Books.getCount() + " books...");
        System.out.println("  to:       "
                + getSharedParameters().getRepositoryFolder());
        System.out.println("  until:    " + getFinishTime().toString());
        System.out.println("  sleeping: " + defaultSleep
                + " milliseconds between each add.");
        System.out.println("  prefix:   " + getPrefix());
        System.out.println();
    }

    /**
     * Add the book to the document object.
     * 
     * @param book
     *            Add this book to the document.
     * @param count
     *            Use this counter to create a unique key.
     * @return A document data object.
     * @throws LuceneException
     *             If the document data object cannot be created.
     */
    private Document getDocument(final Book book, final int count)
            throws LuceneException {
        org.apache.lucene.document.Document doc = new org.apache.lucene.document.Document();
        doc.add(createField("author", book.getAuthor()));
        doc.add(createField(Constants.TITLE, book.getTitle()));
        doc.add(createField(Constants.KEY, getKey(book, count)));
        doc.add(createField("keytype", Constants.KEY_TYPE));
        doc.add(createField("metadata", String.valueOf(book.getRank())));
        return doc;
    }

    /**
     * Create a Lucene field object.
     * 
     * @param fieldName
     *            The name of the field.
     * @param text
     *            The text for the field.
     * @return A Lucene field object.
     */
    private org.apache.lucene.document.Field createField(
            final String fieldName, final String text) {
        return new org.apache.lucene.document.Field(fieldName, text,
                org.apache.lucene.document.Field.Store.YES,
                org.apache.lucene.document.Field.Index.TOKENIZED);
    }

    /**
     * Get a unique key for this document.
     * 
     * @param book
     *            The rank of this book.
     * @param count
     *            Use this counter to create a unique key.
     * @return A key for this document.
     */
    private String getKey(final Book book, final int count) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(getPrefix());
        buffer.append("-");
        buffer.append(count);
        buffer.append("-");
        buffer.append(book.getRank());
        return buffer.toString();
    }

    /**
     * Create a Lucene document collection.
     * 
     * @param collection
     *            The Lucene collection.
     * @throws IOException
     *             If an existing collection cannot be deleted.
     * @throws LuceneException
     *             If a Lucene operation fails.
     */
    private void create(final DocumentCollection collection)
            throws IOException, LuceneException {
        File folder = new File(getSharedParameters().getRepositoryFolder());
        if (folder.exists()) {
            FileUtils.deleteDirectory(folder);
            System.out.println("Deleted [" + folder + "]");
        }
        collection.create();
        System.out.println("Created [" + folder + "]");
    }

    /**
     * Get the prefix which is used to build a unique key for documents in this
     * batch.
     * 
     * @return The prefix.
     */
    private String getPrefix() {
        return parameters.getPrefix();
    }

    /**
     * Does the Lucene repository need to be created?
     * 
     * @return <code>true</code>if the Lucene repository needs to be created.
     */
    private boolean isCreate() {
        return parameters.isCreate();
    }

    /**
     * Create the parameters for this class.
     * 
     * @param args
     *            command line parameters.
     */
    protected void createParameters(final String[] args) {
        parameters = new WriteParameters(this.getClass().getName(), args);
    }

    /** @return the parameters. */
    protected SharedParameters getSharedParameters() {
        return parameters;
    }
}
